import json

from ..sensor import units

__all__ = ["CustomSensorResult"]

from ..error import MaximumChannelCountExceededError


class CustomSensorResult(object):
    """
    Custom Sensor Result.

    Construct PRTG custom sensor results and serialize them to JSON for use
    with PRTG Custom Advanced Sensors.

    See also:
     - https://www.paessler.com/manuals/prtg/custom_sensors
     - https://www.paessler.com/manuals/prtg/python_script_advanced_sensor
    """

    _MAX_CHANNELS = 50

    _channels: list = []
    _text: str = ""
    _error_text: str or None = ""
    _has_error: bool = False

    def __init__(self, text="OK"):
        """
        Creates a sensor result instance with an empty list of channels and a
        default sensor text of "OK", indicating success.

        Add channels to be returned by this sensor result instance with the
        add_channel() and add_primary_channel() methods.

        Add error texts with the error property.

        @param text: The sensor result text. Default is "OK".
        @type text: str
        """
        self._channels = []
        self._text = text
        self._error_text = None
        self._has_error = False

    def add_channel(
            self,
            name,
            value,
            is_primary_channel=False,
            unit=units.ValueUnit.CUSTOM,
            speed_size=units.ValueSize.ONE,
            volume_size=units.ValueSize.ONE,
            speed_time=units.ValueTime.SECOND,
            mode=units.ValueMode.ABSOLUTE,
            is_float=False,
            decimal_mode=None,
            is_warning=False,
            show_chart=True,
            show_table=True,
            is_limit_mode=False,
            limit_max_error=None,
            limit_max_warning=None,
            limit_min_error=None,
            limit_min_warning=None,
            limit_error_msg=None,
            limit_warning_msg=None,
            value_lookup=None,
            notify_changed=False,
    ):
        """
        Adds a channel to the sensor result.

        If no primary channel is added, the first created channel will
        automatically become the primary channel.

        Note that PRTG limits the maximum number of channels to 50.

        See also:
         - https://www.paessler.com/manuals/prtg/custom_sensors#advanced_elements

        @param name: The name of the channel.
        @type name: str
        @param value: The sensor value for the channel.
        @type value: float or int
        @param is_primary_channel: If True, adds the channel as primary
                                   channel.
        @type is_primary_channel: bool
        @param unit: The sensor value's unit. If the unit is not a known PRTG
                     unit, the unit is set to CUSTOM and the custom unit is
                     set to the string in unit. Default is CUSTOM with custom
                     unit set to '#'.
        @type unit: ValueUnit or str
        @param speed_size: Size used for the display value. For example, if
                           you have a value of 50000 and use KILO as size,
                           the display is 50 kilo #. Default is ONE (the value
                           is used as-is). For the Bytes and Speed units, this
                           will be overridden by the setting in the PRTG user
                           interface.
        @type speed_size: ValueSize
        @param volume_size: Size used for the display value. For example, if
                            you have a value of 50000 and use KILO as size,
                            the display is 50 kilo #. Default is ONE (the value
                            is used as-is). For the Bytes and Speed units, this
                            will be overridden by the setting in the PRTG user
                            interface.
        @type volume_size: ValueSize
        @param speed_time: Time used for the display value. For example, if
                           you have a value of 50000 and use SECOND as unit,
                           the display is 50 seconds #. Default is SECOND.
        @type speed_time: ValueTime
        @param mode: Set to ABSOLUTE if the value is an absolute value. Set to
                     DIFFERENCE if the value is a counter. Default is ABSOLUTE.
        @type mode: ValueMode
        @param is_float: True, if the sensor value represents a floating point
                         value, False, if not. If you pass an int value with
                         is_float set to True, it will be converted to float.
                         Default is True.
        @type is_float: bool
        @param decimal_mode: Init value for the Decimal Places option. Use
                             AUTO for integer values, and ALL for float values.
                             Default is None.
        @type decimal_mode: DecimalMode
        @param is_warning: Setting this to True for any channel will put the
                           whole sensor into a "Warning" state. Default is
                           False.
        @type is_warning: bool
        @param show_chart: Init value for the 'Show in graphs' option. Default
                           is True.
        @type show_chart: bool
        @param show_table: Init value for the 'Show in graphs' option. Default
                           is True.
        @type show_table: bool
        @param is_limit_mode: If True, adds the values of *limit_max_error*,
                              *limit_max_warning*, *limit_min_error*,
                              *limit_min_warning*, as well as any given
                              *limit_error_msg* and *limit_warning_msg* to
                              the channel. Default is False (no limits, and
                              all limit values and messages will be ignored).
        @type is_limit_mode: bool
        @param limit_max_error: Upper error limit for the channel. Requires
                                *is_limit_mode=True*. Values will be converted
                                to str in the result. Default is None.
        @type limit_max_error: int or float or str
        @param limit_max_warning: Upper warning limit for the channel.
                                  Requires *is_limit_mode=True*. Values will
                                  be converted to str in the result. Default
                                  is None.
        @type limit_max_warning: int or float or str
        @param limit_min_error: Lower error limit for the channel. Requires
                                *is_limit_mode=True*. Values will be converted
                                to str in the result. Default is None.
        @type limit_min_error: int or float or str
        @param limit_min_warning: Lower warning limit for the channel.
                                  Requires *is_limit_mode=True*. Values will
                                  be converted to str in the result. Default
                                  is None.
        @type limit_min_warning: int or float or str
        @param limit_error_msg: Defines an additional message to be added to
                                the sensor's message when entering a Down
                                status. Requires *is_limit_mode=True*. Default
                                is None.
        @type limit_error_msg: str
        @param limit_warning_msg: Defines an additional message to be added to
                                  the sensor's message when entering a Warning
                                  status. Requires *is_limit_mode=True*.
                                  Default is None.
        @type limit_warning_msg: str
        @param value_lookup: ID of a lookup file to use, e.g. to convert integer
                             status return values into actual names. Default is
                             None.
        @type value_lookup: str
        @param notify_changed: Setting this to True on any channel will trigger
                               a change notification which can be processed
                               with a PRTG Change Trigger to send a
                               notification. Default is False.
        @type notify_changed: bool
        """
        if len(self._channels) == self._MAX_CHANNELS:
            raise MaximumChannelCountExceededError(
                "You cannot add more than " "50 channels."
            )

        channel = dict()

        if name is None or type(name) is not str:
            raise ValueError("Channel name is mandatory (str)")

        channel["Channel"] = str(name)

        if value is None or type(value) not in (int, float):
            raise ValueError("Channel value is mandatory (int, float)")

        if is_float is None:
            if type(value) is float:
                is_float = True
            else:
                is_float = False

        if is_float:
            channel["Value"] = float(value)
            channel[
                "DecimalMode"
            ] = units.DecimalMode.ALL
            channel["Float"] = 1
        else:
            channel["Value"] = int(value)

        if (
                unit is None
                or unit == units.ValueUnit.CUSTOM
        ):
            channel["Unit"] = units.ValueUnit.CUSTOM
            channel["CustomUnit"] = "#"
        else:
            try:
                channel_unit = units.ValueUnit(unit)
                channel["Unit"] = channel_unit
            except ValueError:
                channel[
                    "Unit"
                ] = units.ValueUnit.CUSTOM
                channel["CustomUnit"] = unit

        if speed_size is not None:
            channel["SpeedSize"] = units.ValueSize(
                speed_size
            )

        if volume_size is not None:
            channel["VolumeSize"] = units.ValueSize(
                volume_size
            )

        if speed_time is not None:
            channel["SpeedTime"] = units.ValueTime(
                speed_time
            )

        if mode is not None:
            channel["Mode"] = units.ValueMode(mode)

        if decimal_mode is not None:
            channel[
                "DecimalMode"
            ] = units.DecimalMode(decimal_mode)

        if is_warning:
            channel["Warning"] = 1

        if not show_chart:
            channel["ShowChart"] = 0

        if not show_table:
            channel["ShowTable"] = 0

        if is_limit_mode:
            # LimitMode will only be set if and only if any limit is set
            if limit_max_error is not None:
                channel["LimitMaxError"] = str(limit_max_error)
                channel["LimitMode"] = 1
            if limit_max_warning is not None:
                channel["LimitMaxWarning"] = str(limit_max_warning)
                channel["LimitMode"] = 1
            if limit_min_error is not None:
                channel["LimitMinError"] = str(limit_min_error)
                channel["LimitMode"] = 1
            if limit_min_warning is not None:
                channel["LimitMinWarning"] = str(limit_min_warning)
                channel["LimitMode"] = 1
            if limit_error_msg is not None:
                channel["LimitErrorMsg"] = str(limit_error_msg)
                channel["LimitMode"] = 1
            if limit_warning_msg is not None:
                channel["LimitWarningMsg"] = str(limit_warning_msg)
                channel["LimitMode"] = 1

        if value_lookup:
            channel["ValueLookup"] = value_lookup

        if notify_changed:
            channel["NotifyChanged"] = ""

        if is_primary_channel:
            self._channels.insert(0, channel)
        else:
            self._channels.append(channel)

    def add_primary_channel(self, name, value, **kwargs):
        """
        Adds a primary channel to the sensor result.

        This convenience method is equivalent to
        *add_channel(..., primary_channel=True)*.

        Note that subsequent calls of this method will push the
        previously set primary channel one position backwards.

        Refer to *add_channel()* for a list of valid keyword arguments.

        @param name: The name of the channel.
        @type name: str
        @param value: The sensor value for the channel.
        @type value: float or int
        """
        self.add_channel(name=name, value=value, is_primary_channel=True, **kwargs)

    @property
    def text(self):
        """
        Returns the sensor text.

        @return: A string denoting the sensor text. If no error
                 occurred, it should read "OK".
        @rtype: str
        """
        return self._text

    @text.setter
    def text(self, text):
        """
        Sets the sensor text.

        @param text: A string denoting the sensor text. If no error
                     occurred, it should be set to "OK".
        @type text: str
        """
        if text is None:
            self._text = ""
        else:
            self._text = text

    @property
    def error(self):
        """
        Returns the sensor error text.

        @return: A string denoting the sensor error text.
        @rtype: str
        """
        return self._error_text

    @error.setter
    def error(self, error_text):
        """
        Adds an error text to the sensor result.

        Note that although the channel data will not be serialized in the,
        sensor result, the channel data will be preserved. If error is
        subsequently set to None, the error is cleared and the channel data
        will reappear in the serialized sensor result.

        @param error_text: The error text to be displayed in PRTG. If
                           error_text is set to None, the error is cleared.
        @type error_text: str or None
        """
        self._error_text = error_text
        self._has_error = True if error_text is not None else False

    @property
    def result(self):
        """
        Returns a dict containing the sensor result.

        The structure of the dict is equivalent to the JSON structure the
        sensor expects as input and can directly be serialized into JSON or
        XML.

        @return: A dict containing the sensor result.
        @rtype: dict
        """
        if self._has_error:
            result = {"prtg": {"text": self._error_text, "error": 1}}
        else:
            result = {"prtg": {"text": self._text, "result": self._channels}}

        return result

    @property
    def json_result(self):
        """
        Returns a JSON string containing the serialized sensor result.

        The JSON result can be passed to the sensor as-is.

        @return: A JSON string containing the serialized sensor result.
        @rtype: str
        """
        return json.dumps(self.result)

    def __str__(self):
        """
        Returns a JSON-encoded string representation of the sensor result.

        @return: A JSON string containing the serialized sensor result.
        @rtype: str
        """
        return self.json_result
